package birdsong.server;

import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;

import Spam.spamFilter;
import birdsong.comm.ServerComm;
import birdsong.comm.ServerCommunicator;
import birdsong.comm.ServerSideMessage;


public class Server implements BirdsongServer{
	/**
	 * LinkedList of all the connected users (Clients) 
	 */
	private LinkedList<Client> onlineClientsList = new LinkedList<Client>();
	private ServerComm serverComm;

	// STANDARD ERROR MESSAGES
	public static final String ERROR_CLIENT_OFFLINE = "ERROR - Client offline";
	public static final String ERROR_FOLLOWER_D_OFFLINE = "ERROR - Follower(d) offline";
	public static final String ERROR_PAYLOAD_NULL = "ERROR - Payload is null";
	public static final String ERROR_SERVERCOMM_NULL = "ERROR - serverComm is null";
	
	public static final String ERROR_MESSAGE_NULL = "ERROR - serverComm.getNextMessage() returns null";
	public static final String ERROR_DUPLICATED_NICKNAME = "";

	private ArrayList<String> errorLog = new ArrayList<String>();
	private int numberOfErrors = 0; 
	@Override
	/**
	 * Starts the server which receives and processes messages and keeps track of the clients
	 * @author Diana, Ines, Rafael, Ricardo
	 * @param serverComm the serverComm through which the server should communicate with clients.
	 *
	 */
	public void start(ServerComm serverComm) {
		numberOfErrors = 0;
		errorLog.clear();
		this.serverComm = serverComm;
	
		//COMMENT
		boolean stopped = false;
		if(serverComm != null){
			serverComm.start();
			while (!stopped) {

				//	Getting the message, if any
				ServerSideMessage message = this.serverComm.getNextMessage();
				if(message != null){
					// BIRDSONG_MESSAGE
					if (message.getType() == ServerSideMessage.Type.BIRDSONG_MESSAGE) {
						try{
							processBirdSongMessage(message);
						
						} catch (IllegalArgumentException ex){
							if(ex.getMessage().equals(ERROR_CLIENT_OFFLINE)){
								System.out.println(ERROR_CLIENT_OFFLINE);
							} else {
								if(ex.getMessage().equals(ERROR_PAYLOAD_NULL)){
									serverComm.sendError(message.getClientNickname(), ERROR_PAYLOAD_NULL);
								}
							}
							errorLog.add(ex.getMessage());
							numberOfErrors++;
						}
					}

					// CLIENT_CONNECTED
					else if (message.getType() == ServerSideMessage.Type.CLIENT_CONNECTED) {
						processClientConnectedMessage(message);
					}

					// CLIENT_DISCONNECTED
					else if (message.getType() == ServerSideMessage.Type.CLIENT_DISCONNECTED) {
						try{
							processClientDisconnectedMessage(message);
						
						} catch (IllegalArgumentException ex){
							if(ex.getMessage().equals(ERROR_CLIENT_OFFLINE)){
								errorLog.add(ERROR_CLIENT_OFFLINE);
								numberOfErrors++;
							}
						}

					}

					// FOLLOW
					else if (message.getType() == ServerSideMessage.Type.FOLLOW) {
						try{
							processFollowMessage(message);
						
						} catch (IllegalArgumentException ex){
							if(ex.getMessage().equals(ERROR_CLIENT_OFFLINE)){
								System.out.println(ERROR_CLIENT_OFFLINE);
							} else {
								if(ex.getMessage().equals(ERROR_PAYLOAD_NULL)){
									serverComm.sendError(message.getClientNickname(), ERROR_PAYLOAD_NULL);
								} else {
									if(ex.getMessage().equals(ERROR_FOLLOWER_D_OFFLINE)){
										serverComm.sendError(message.getClientNickname(), ERROR_FOLLOWER_D_OFFLINE);
									}
								}
							}
							errorLog.add(ex.getMessage());
							numberOfErrors++;
						}
					}

					// UNFOLLOW
					else if (message.getType() == ServerSideMessage.Type.UNFOLLOW) {
						try{
							processUnfollowMessage(message);
						
						} catch (IllegalArgumentException ex){
							if(ex.getMessage().equals(ERROR_CLIENT_OFFLINE)){
								/* The Network communications service 
								 * shouldn't allow to send a message without a sender.
								 * But the service shouldn't go down because of it
								 */
								System.out.println(ERROR_CLIENT_OFFLINE);
							} else {
								if(ex.getMessage().equals(ERROR_PAYLOAD_NULL)){
									serverComm.sendError(message.getClientNickname(), ERROR_PAYLOAD_NULL);
								} else {
									if(ex.getMessage().equals(ERROR_FOLLOWER_D_OFFLINE)){
										serverComm.sendError(message.getClientNickname(), ERROR_FOLLOWER_D_OFFLINE);
									}
								}
							}
							errorLog.add(ex.getMessage());
							numberOfErrors++;
						}
					}
				} else {
					errorLog.add(ERROR_MESSAGE_NULL);
					numberOfErrors++;
					stopped = true;
				}
			}
		} else {
			errorLog.add(ERROR_SERVERCOMM_NULL);
			numberOfErrors++;
			throw new IllegalArgumentException(ERROR_SERVERCOMM_NULL);
		}
	}
	/**
	 * Updates the relationship between the asker and the followed, that is, asker unfollows the given client
	 * @param message The message to be processed (must be from UNFOLLOW type and must have a payload)
	 * @throws IllegalArgumentException Throws an exception whenever:
	 * The message hasn't a payload;
	 * The client isn't online;
	 * The desired followed isn't online;
	 */
	private void processUnfollowMessage(ServerSideMessage message) throws IllegalArgumentException{
		//	Checking the preconditions
		assert message.getType() == ServerSideMessage.Type.UNFOLLOW;

		// The client is asking to unfollow the user on the payload
		String askerNickname = message.getClientNickname();
		String followedNickname = message.getPayload();
		// Checking if payload has text
		if(message.getPayload() != null){
			Client asker = getClientByNickname(askerNickname);
			//	Checking if the asker is online - Precondition
			if(asker != null){
				Client followed = getClientByNickname(followedNickname);
				// Checking if the choosen client to follow is online
				if(followed != null){
					asker.removeFollowed(followed);
					followed.removeFollower(asker);			

					// Warn the followed client about what happened;

					serverComm.sendNewUnfollow(followedNickname, askerNickname);
				}
				else {
					throw new IllegalArgumentException(ERROR_FOLLOWER_D_OFFLINE);
				}
			}
			else {
				throw new IllegalArgumentException(ERROR_CLIENT_OFFLINE);
			}
		}
		else {
			throw new IllegalArgumentException(ERROR_PAYLOAD_NULL);
		}
	}
	/**
	 * Updates the relationship between the asker and the followed, that is, asker unfollows the given client
	 * @param message The message to be processed (must be from FOLLOW type and must have a payload)
	 * @throws IllegalArgumentException Throws an exception whenever:
	 * The message hasn't a payload;
	 * The client isn't online;
	 * The desired followed isn't online;
	 */
	private void processFollowMessage(ServerSideMessage message) throws IllegalArgumentException{
		// The client is asking to follow the user on the payload
		assert message.getType() == ServerSideMessage.Type.FOLLOW;
		if(message.getPayload() != null){
			String askerNickname = message.getClientNickname();
			String followedNickname = message.getPayload();
			Client asker = getClientByNickname(askerNickname);

			// Checking if asker is online
			if(asker != null){
				Client followed = getClientByNickname(followedNickname);
				if(followed != null){
					asker.addFollowed(followed);
					followed.addFollower(asker);

					//Warn the followed about what happened;

					serverComm.sendNewFollower(followedNickname, askerNickname);
				}
				else{
					throw new IllegalArgumentException(ERROR_FOLLOWER_D_OFFLINE);
				}
			} else {
				throw new IllegalArgumentException(ERROR_CLIENT_OFFLINE);
			}
		} else {
			throw new IllegalArgumentException(ERROR_PAYLOAD_NULL);
		}
	}

	/**
	 * Handles the disconnection of a client (sends a message to all users)
	 * @param message The message to be processed (must be from CLIENT_DISCONNECTED type)
	 * @throws IllegalArgumentException Throws an exception whenever:
	 * The client isn't online;
	 */
	private void processClientDisconnectedMessage(ServerSideMessage message) throws IllegalArgumentException{
		assert message.getType() == ServerSideMessage.Type.CLIENT_DISCONNECTED;
		String nickname = message.getClientNickname();

		Client clientGoing = getClientByNickname(nickname);
		//Checking if client is online
		if(clientGoing != null){
			//Delete client from the online client list
			onlineClientsList.remove(clientGoing);
			
			//Warn the all the users about the disconnection
			for(Client client : onlineClientsList){
				String clientNickname = client.getNickname();
				serverComm.sendClientDisconnected(clientNickname, nickname);
			}
			
			
		} 
		else {
			throw new IllegalArgumentException(ERROR_CLIENT_OFFLINE);
		}
	}
	/**
	 * Handles the connection of a new client
	 * @param message The message to be processed (must be from CLIENT_CONNECTED type)
	 */
	private void processClientConnectedMessage(ServerSideMessage message){
		assert message.getType() == ServerSideMessage.Type.CLIENT_CONNECTED;

		String nickname = message.getClientNickname();

		boolean nicknameIsAvailable = checkNicknameAvailability(nickname);

		// Checking if the choosen nickname is available, that is, if it isn't already being used
		if(nicknameIsAvailable){
			Client justArrived = new Client(nickname);

			// 1) Warn every one that there is a new client 
			// 2) And send the list of online users to the new client
				

			for(Client client : onlineClientsList){
				serverComm.sendClientConnected(client.getNickname(), nickname);
				serverComm.sendClientConnected(nickname, client.getNickname());
			}
			
			// Add the client to the online clients list
			onlineClientsList.add(justArrived);

		}
		else {
			serverComm.sendError(nickname, ERROR_DUPLICATED_NICKNAME);
		}
	}

	/**
	 * Checks if the nickname is available, that is, if the nickname isn't already being used
	 * @param nickname
	 * @return
	 */
	private boolean checkNicknameAvailability(String nickname) {
		// If there isn't a client with the specified nickname, it's available
		return getClientByNickname(nickname) == null;
	}
	/**
	 * Sends the received Birdsong message to all the followers
	 * @param message The message to be processed (must be from BIRDSONG_MESSAGE type 
	 * and must have a payload)
	 * @throws IllegalArgumentException Throws an exception whenever:
	 * The message hasn't a payload;
	 * The client isn't online;
	 */
	private void processBirdSongMessage(ServerSideMessage message) throws IllegalArgumentException{

		assert message.getType() == ServerSideMessage.Type.BIRDSONG_MESSAGE;

		String nickname = message.getClientNickname(); 
		String birdsongMessage = message.getPayload();
		// Check if birdsongMessade has content
		if(birdsongMessage != null){
			// Get the client

			Client client = getClientByNickname(nickname);

			// Checking if client is online

			if(client != null){
				// Get the followersList

				LinkedList<Client> followersList = client.getFollowersList();

				// Send the message to all the followers

				for(Client follower : followersList){
					String followerNickname = follower.getNickname();
					serverComm.sendBirdsongMessage(nickname, followerNickname, birdsongMessage);
				}
			}
			else {
				throw new IllegalArgumentException(ERROR_CLIENT_OFFLINE);
			}
		}
		else{
			throw new IllegalArgumentException(ERROR_PAYLOAD_NULL);
		}
	}


	/**
	 * Searches and returns the online client with the given nickname
	 * @param nickname
	 * @return Null if the client isn't online. Returns the online client with the given nickname
	 */

	private Client getClientByNickname(String nickname){		

		Client foundClient = null;
		Iterator<Client> iterator = onlineClientsList.iterator();
		boolean found = false;
		boolean ended = !iterator.hasNext();

		while(!found && !ended){
			Client client = iterator.next();
			if(client.getNickname().equals(nickname)){
				found = true;
				foundClient = client;
			} else {
				if(!iterator.hasNext()){
					ended = true;
				}
			}
		}

		return foundClient;
	}
	/**
	 * Gives access to the Error Log
	 * @return errorLog An array of Error description
	 */
	public ArrayList<String> getErrorLog(){
		return errorLog;
	}
	
	/**
	 * Returns the error count during the last execution
	 * @return numberOfErrors the number of errors that occurred during the last execution
	 */
	public int getNumberOfErrors(){
		return numberOfErrors;
	}
	
	public static void main(String[] args){
		try {
			InetAddress thisIp =InetAddress.getLocalHost();
			System.out.println(thisIp.getHostAddress());
		} catch (UnknownHostException e) {
			e.printStackTrace();
		}
		ServerCommunicator serverComm = new ServerCommunicator();
		spamFilter spamFilter = new spamFilter(serverComm);
		Server server = new Server();
		server.start(spamFilter);

	}
}
